package edu.utexas.esel.jgraphpad.archpadplugin.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EventObject;
import java.util.List;

import javax.swing.AbstractCellEditor;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.undo.UndoableEdit;

import org.jgraph.JGraph;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.w3c.dom.Node;

import com.jgraph.editor.JGraphEditorResources;
import com.jgraph.editor.factory.JGraphEditorFactoryMethod;
import com.jgraph.pad.graph.JGraphpadGraph;
import com.jgraph.pad.util.JGraphpadFocusManager;

import edu.utexas.esel.jgraphpad.archpadplugin.Utils;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ElementGraphCell;
import edu.utexas.esel.jgraphpad.archpadplugin.model.ArchPadDiagram;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element;
import edu.utexas.esel.jgraphpad.archpadplugin.model.RegionManager;
import edu.utexas.esel.jgraphpad.archpadplugin.model.RegionManager.RegionListener;

/**
 * This class is a tab for managing regions in ArchPad models.
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class RegionTree extends JTree implements TreeSelectionListener,
        RegionListener, UndoableEditListener, FocusListener {

	private static final long serialVersionUID = 1L;

	/**
	 * The key for the name of the regions tab.
	 */
	public static final String KEY_REGIONS_TAB_NAME = "regionsTabName";

	DefaultTreeModel mTreeModel;

    private UndoableEdit mLastUndoableEdit;

	private DefaultGraphModel mGraphModel;

	private RegionManager mRegionManager;

	private GraphLayoutCache mCache;

	public RegionTree() {
		super(new DefaultTreeModel(new DefaultMutableTreeNode()));
		mTreeModel = (DefaultTreeModel) getModel();
        configure();
        registerFocusedGraphChangeListener();
	}

	/**
	 * Configures this instance.
	 */
	private void configure() {
		setRootVisible(false);
        setEditable(true);
        setCellRenderer(new TreeCellRenderer());
        setCellEditor(new TreeCellElementEditor());
        addTreeSelectionListener(this);
        addFocusListener(this);
        getSelectionModel().setSelectionMode(
			    TreeSelectionModel.SINGLE_TREE_SELECTION);
	}

    /**
     * Register listener in the {@link KeyboardFocusManager} for the focus
     * change event of the permanent focus owner.
     */
	private void registerFocusedGraphChangeListener() {
		final RegionTree self = this;

		JGraphpadFocusManager focusManager =
			JGraphpadFocusManager.getCurrentGraphFocusManager();
		focusManager.addPropertyChangeListener(new PropertyChangeListener() {

			public void propertyChange(PropertyChangeEvent e) {
				if (e.getPropertyName().equals(
						JGraphpadFocusManager.FOCUSED_GRAPH_PROPERTY)) {

					// this tab works only with ArchPad documents
					boolean archPadDocument = Utils.isCurrentArchPadDiagram();
					setEnabled(archPadDocument);
					if (!archPadDocument) {
						emptyTree();
						return;
					}

					if (e.getOldValue() instanceof JGraphpadGraph) {
						if (mRegionManager != null) {
							mRegionManager.removeRegionListener(self);
						    mRegionManager = null;
						    mCache = null;
						}

						emptyTree();
					}
					if (e.getNewValue() instanceof JGraphpadGraph) {
						JGraphpadGraph graph = (JGraphpadGraph) e.getNewValue();
						mCache = graph.getGraphLayoutCache();

						ArchPadDiagram diagram =
							Utils.getPermanentFocusOwnerArchPadDiagram();
						mRegionManager = diagram.getRegionManager(); 
						mRegionManager.addRegionListener(self);

						populateTree();
					}
			    }
			}
		});
	}

	/**
	 * Makes the tree empty by removing all children of the invisible root.
	 */
	private void emptyTree() {
		DefaultTreeModel model = (DefaultTreeModel) getModel();
		DefaultMutableTreeNode root = (DefaultMutableTreeNode) getRoot();
		for (int i = root.getChildCount() - 1; i >=0 ; i--) {
			DefaultMutableTreeNode child =
				(DefaultMutableTreeNode) root.getChildAt(i);
			model.removeNodeFromParent(child);
		}
	}

	/**
	 * Populate the tree with all regions of the focused diagram.
	 */
	private void populateTree() {
	    List<ElementGraphCell> regions = mRegionManager.getRegions();
	    for (ElementGraphCell region : regions) {
	    	addRegion(region);
	    }
	}

	@Override
	public void focusGained(FocusEvent e) {
		tryRegisterUndoableEditListener();
	}
	
	@Override
	public void focusLost(FocusEvent e) {
		tryUnregisterUndoableEditListener();
		tryUndoUndoableEdit();
	}

    @Override
	public void undoableEditHappened(UndoableEditEvent event) {
    	mLastUndoableEdit = event.getEdit();
	}

    /**
     * Tries to register an {@link UndoableEditListener} if such is not
     * already registered.
     */
    private void tryRegisterUndoableEditListener() {
	    if (mGraphModel == null) {
		    JGraph graph = Utils.getPermanentFocusOwnerGraph();
		    if (graph != null) {
		        mGraphModel = (DefaultGraphModel) graph.getModel();
		        mGraphModel.addUndoableEditListener(this);
		    }
	    }
    }

    /**
     * Tries to unregister an {@link UndoableEditListener} if such is
     * registered.
     */
    private void tryUnregisterUndoableEditListener() {
		if (mGraphModel != null) {
		    mGraphModel.removeUndoableEditListener(this);
		    mGraphModel = null;
		}
    }

    /**
     * Tries to undo the last {@link UndoableEdit} if such and if it is
     * undoable.
     */
    private void tryUndoUndoableEdit() {
    	if (mLastUndoableEdit != null) {
    		if (mLastUndoableEdit.canUndo()) {
			    mLastUndoableEdit.undo();
    		}
			mLastUndoableEdit = null;
		}
    }
    
	@Override
	public void valueChanged(TreeSelectionEvent event) {        	
	    // If a node is selected for the first time the selection event is
	    // fired before the focus event. We want to register undoable event
	    // listener if not already registered while gaining the focus.
	    tryRegisterUndoableEditListener();
	    
	    // unmark any selected cells if such
	    tryUndoUndoableEdit();

        // if clearing the tree, the manager is null and nothing is to be marked    
	    if (mRegionManager == null) {
	        return;	
	    }

		DefaultMutableTreeNode selection =
	    	(DefaultMutableTreeNode) event.getPath().getLastPathComponent();
        ElementGraphCell cell = (ElementGraphCell) selection.getUserObject();

        if (cell != null) {
		    String[] keys = new String[] {GraphConstants.BORDERCOLOR,
		        GraphConstants.LINEWIDTH};
		    Object[] values = new Object[] {Color.RED, 2.0f}; 

		    List<ElementGraphCell> elements = mRegionManager.getMembers(cell);
		    if (elements == null) {
			    Utils.setAttributes(mCache,
			    	new Object[] {selection.getUserObject()} , keys, values);
		    } else {
			    Utils.setAttributes(mCache, elements.toArray(), keys, values);
		    }
		}
	}

	@Override
	public void onCreate(ElementGraphCell region) {
		addRegion(region);
	}

	/**
	 * Adds a region to the tree.
	 *
	 * @param region The region.
	 */
	private void addRegion(ElementGraphCell region) {
		DefaultMutableTreeNode root = getRoot();
		DefaultMutableTreeNode child = appendWrappedCellTreeNode(region, root);

		for (ElementGraphCell element : mRegionManager.getMembers(region)) {
        	appendWrappedCellTreeNode(element, child);
        }
	}

	/**
	 * Appends a new tree node as the last child of its
	 * <code>parent</code> with the <code>element</code> as a user
	 * object.
	 *
	 * @param element The user object.
	 * @param parent The parent node.
	 */
	private DefaultMutableTreeNode appendWrappedCellTreeNode(
			ElementGraphCell element, DefaultMutableTreeNode parent) {
		// We need to wrap the graph cell to avoid the tree changing the graph 
		DefaultMutableTreeNode child = new DefaultMutableTreeNode(element);
	    mTreeModel.insertNodeInto(child, parent, parent.getChildCount());
	    scrollPathToVisible(new TreePath(child.getPath()));
	    return child;
    }

	@Override
	public void onModify(ElementGraphCell region) {
		// TODO Auto-generated method stub		
	}

	@Override
	public void onRemove(ElementGraphCell region) {
//		MutableTreeNode root = (MutableTreeNode) mTreeModel.getRoot();
//		for (int i = 0, count = root.getChildCount(); i < count; i++) {
//			DefaultMutableTreeNode child =
//				(DefaultMutableTreeNode) root.getChildAt(i);
//			if (child.getUserObject().equals(region)) {
//				mTreeModel.removeNodeFromParent(child);
//				return;
//			}
//		}
	}

	/**
	 * Convenience method for getting the model root.
	 *
	 * @return The root.
	 */
	private DefaultMutableTreeNode getRoot() {
		return (DefaultMutableTreeNode) mTreeModel.getRoot();
	}

	/**
	 * Class for editing tree nodes in the regions tab.
	 */
	
	/**
	 * This class is editor component for cells that contain {@link Element}.
	 */
	private static class TreeCellElementEditor extends AbstractCellEditor
	        implements TreeCellEditor {

		private static final long serialVersionUID = 1L;

		private ElementEditor mEditor = new ElementEditor();

		private ElementGraphCell mEditedCell;

		@Override
		public boolean isCellEditable(EventObject event) {
			if (event instanceof MouseEvent) {
				MouseEvent mouseEvent = (MouseEvent) event;
                if (SwingUtilities.isLeftMouseButton(mouseEvent)) {
                	return (mouseEvent.getClickCount() == 3);
                }
			}
			return false;
		}

		@Override
		public Object getCellEditorValue() {
			mEditedCell.setUserObject(mEditor.getCurrentValue());
			return mEditedCell;
		}

		@Override
		public Component getTreeCellEditorComponent(JTree tree,
				Object value, boolean selected, boolean expanded,
				boolean leaf, int row) {

			DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
			toggleEditedNodeStateWorkaround(tree, node);

        	mEditedCell = (ElementGraphCell) node.getUserObject();
			Element element = Utils.getCellElement(mEditedCell);
			mEditor.initialize(this, element) ;
			mEditor.setVisible(true);

			return mEditor;
		}

		/**
		 * Toggles the state of the edited node since we start an edit after
		 * three click but toggle nodes after two clicks. Hence, a user who
		 * wants to edit performs an inadvertent toggle.
		 *
		 * @param tree The tree that contains the node.
		 * @param node The node whose state to toggle.
		 */
		private void toggleEditedNodeStateWorkaround(JTree tree,
				DefaultMutableTreeNode node) {
        	RegionTree regionsTree = (RegionTree) tree;
        	TreePath treePath = new TreePath(node.getPath());
        	boolean state = !tree.isExpanded(tree.getRowForPath(treePath));
        	regionsTree.setExpandedState(treePath, state);
		}
	}

	/**
	 * Factory method that creates the regions tab.
	 */
	public static class FactoryMethod extends JGraphEditorFactoryMethod {

		/**
		 * Defines the default name for factory methods of this kind.
		 */
		public static final String NAME = "createRegionsTab";

		public FactoryMethod(String name) {
			super(name);
		}

		@Override
		public Component createInstance(Node configuration) {
	        return new RegionTree();
		}
	}

	/**
     * Renderer for showing type specific icons in the tree.
     */
    private class TreeCellRenderer extends DefaultTreeCellRenderer {

    	private static final long serialVersionUID = 1L;

    	private static final String SUFFIX_ICON_KEY = ".icon";

    	@Override
        public Component getTreeCellRendererComponent(JTree tree,
        		Object value, boolean selected, boolean expanded,
        		boolean leaf, int row, boolean hasFocus) {

        	super.getTreeCellRendererComponent(tree, value, selected,
                expanded, leaf, row, hasFocus);

        	DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
        	ElementGraphCell cell = (ElementGraphCell) node.getUserObject();

        	if (cell != null) {
        		String type = cell.getAttribute(
        			ElementGraphCell.ATTRIBUTE_ELEMENT_TYPE);
        		String key = type + SUFFIX_ICON_KEY;
        		String uri = JGraphEditorResources.getString(key);

        		if (uri != null) {
        			Icon icon = JGraphEditorResources.getImage(uri);
        			if (icon != null) {
        				setIcon(icon);
        			}
         	    }
        	}

    	    return this;
        }
    }
}
